#include "Camera.h"
#include "SceneNode.h"

namespace Nezha
{
	// for default, we're a opengl es 2.0 rendering engine.
	DepthType Camera::msDepthType = DT_OGL;

	void Camera::_SetDepthType(DepthType dt)
	{
		msDepthType = dt;
	}

	DepthType Camera::GetDepthType()
	{
		return msDepthType;
	}

	Camera::Camera(const _string& name)
		:SceneObject(name)
		,mPerspective(true)
	{
		setFrame(Vector3::ZERO, -Vector3::UNIT_Z, Vector3::UNIT_Y, Vector3::UNIT_X);
		setFrustum(60.0f, 1.0f, 0.1f, 1000.0f);
	}

	Camera::Camera(const _string& name, bool perspective)
		:SceneObject(name)
		,mPerspective(perspective)
	{
		setFrame(Vector3::ZERO, -Vector3::UNIT_Z, Vector3::UNIT_Y, Vector3::UNIT_X);
		setFrustum(60.0f, 1.0f, 0.1f, 1000.0f);
	}

	Camera::~Camera()
	{

	}

	void Camera::setFrame(const Vector3& position, const Vector3& dir, const Vector3& up, const Vector3& right)
	{
		if(!getSceneNode())
			return;

		getSceneNode()->setPosition(position);
		setAxes(dir, up, right);
	}

	void Camera::setAxes(const Vector3& dir, const Vector3& up, const Vector3& right)
	{
		mDirection = dir;
		mUp = up;
		mRight = right;

		float det = fabs(mDirection.dot(mUp.cross(mRight)));
		if(fabs(1.0f - det) > 0.01f)
		{
			Vector3::orthonormalize(mDirection, mUp, mRight);
		}

		_updateViewMatrix();
	}

	void Camera::setFrustum(const Frustum& f)
	{
		memcpy(&mFrustum, &f, sizeof(f));

		_updateProjectionMatrix();
	}

	void Camera::setFrustum(float fovY, float aspect, float _near, float _far)
	{
		float halfAngle = 0.5f * fovY * NZ_DEGTORAD;

		mFrustum.mTop = _near * Tan(halfAngle);
		mFrustum.mBottom = -mFrustum.mTop;
		mFrustum.mRight = aspect * mFrustum.mTop;
		mFrustum.mLeft = -mFrustum.mRight;
		mFrustum.mNear = _near;
		mFrustum.mFar = _far;

		_updateProjectionMatrix();
	}

	void Camera::_updateViewMatrix()
	{
		if(!getSceneNode())
			return;

		/*
					| R |
			M =	| U | [X - P]
					| D |
		*/

		const Vector3& worldPos = getSceneNode()->getWorldPos();

		mViewMat.setRow(0, mRight.x, mRight.y, mRight.z, -worldPos.dot(mRight));
		mViewMat.setRow(1, mUp.x, mUp.y, mUp.z, -worldPos.dot(mUp));
		mViewMat.setRow(2, mDirection.x, mDirection.y, mDirection.z, -worldPos.dot(mDirection));
		mViewMat.setRow(3, 0, 0, 0, 1.0f);

		mPVMat = mProjectionMat * mViewMat;
	}

	void Camera::_updateProjectionMatrix()
	{
		/*
			perspective, z[0, 1]
			x1 = n*x / z => (x1 - (l + r) / 2) / ((r - l) / 2) -> [-1, 1] => v1 = (2*n*x / (r - l) - (r+l)/(r-l)*z) / z
			v2 = (2*n*y/(t-b) - (t+b)/(t-b)*z) / z
			z2 = (a*z + b) / z <=> a + b/z, a + b/n = 0 && a + b/f = 1 => b = n*f / (n - f)  a = f / (f-n)
			v3 = f / (f-n) *z +  n*f / (n-f)
			v4 = <0, 0, 1, 0>

			orthographic, z[0, 1] w = 1
			x1 = x => (x - (l+r)/2) / ((r-l)/2) -> [-1, 1] => v1 = (2*x / (r-l) - (r+l) / (r-l))
			v2 = (2*y / (r-l) - (r+l) / (r-l))
			z => (z - n) / (f- n) -> [0, 1] => v3 = 1 / (f-n) - n / (f-n)
			v4 = <0, 0, 0, 1>
		*/
		float n = mFrustum.mNear;
		float f = mFrustum.mFar;
		float l = mFrustum.mLeft;
		float r = mFrustum.mRight;
		float t = mFrustum.mTop;
		float b = mFrustum.mBottom;

		float invDiff_fn = 1.0f / (f - n);
		float invDiff_rl = 1.0f / (r - l);
		float invDiff_tb = 1.0f / (t - b);
		float Sum_fn_Mul_invDiff_fn = (f + n) * invDiff_fn;
		float Sum_rl_Mul_invDiff_rl = (r + l) * invDiff_rl;
		float Sum_tb_Mul_InvDiff_tb = (t + b) * invDiff_tb;

		if(mPerspective)
		{
			if(msDepthType == DT_DX)
			{
				mProjectionMat.setRow(0, 2.0f * n * invDiff_rl, 0, -1.0f * Sum_rl_Mul_invDiff_rl, 0);
				mProjectionMat.setRow(1, 0, 2.0f * n * invDiff_tb, -1.0f * Sum_tb_Mul_InvDiff_tb, 0);
				mProjectionMat.setRow(2, 0, 0, f * invDiff_fn, -1.0f * n * f * invDiff_fn);
				mProjectionMat.setRow(3, 0, 0, 1, 0);
			}
			else
			{
				/*
					z2 = a + b/z => a + b/n = -1 a + b/f = 1 => a = (f + n)/(f-n) b = -2*n*f /(f-n)
				*/
				mProjectionMat.setRow(0, 2.0f * n * invDiff_rl, 0, -1.0f * Sum_rl_Mul_invDiff_rl, 0);
				mProjectionMat.setRow(1, 0, 2.0f * n * invDiff_tb, -1.0f * Sum_tb_Mul_InvDiff_tb, 0);
				mProjectionMat.setRow(2, 0, 0, Sum_fn_Mul_invDiff_fn, -2.0f * n * f * invDiff_fn);
				mProjectionMat.setRow(3, 0, 0, 1.0f, 0);
			}
		}
		else
		{
			if(msDepthType == DT_DX)
			{
				mProjectionMat.setRow(0, 2.0f * invDiff_rl, 0, 0, -1.0f * Sum_rl_Mul_invDiff_rl);
				mProjectionMat.setRow(1, 0, 2.0f * invDiff_tb, 0, -1.0f * Sum_tb_Mul_InvDiff_tb);
				mProjectionMat.setRow(2, 0, 0, invDiff_fn, -1.0f * n * invDiff_fn);
				mProjectionMat.setRow(3, 0, 0, 0, 1.0f);
			}
			else
			{
				/*
					z2 = (z - (n+f)/2) / ((f-n)/2) -> [-1, 1]
				*/
				mProjectionMat.setRow(0, 2.0f * invDiff_rl, 0, 0, -1.0f * Sum_rl_Mul_invDiff_rl);
				mProjectionMat.setRow(1, 0, 2.0f * invDiff_tb, 0, -1.0f * Sum_tb_Mul_InvDiff_tb);
				mProjectionMat.setRow(2, 0, 0, 2.0f * invDiff_fn, -1.0f * Sum_fn_Mul_invDiff_fn);
				mProjectionMat.setRow(3, 0, 0, 0, 1.0f);
			}
		}

		mPVMat = mProjectionMat * mViewMat;
	}

	void Camera::_setPerspective(bool b)
	{
		if(mPerspective != b)
		{
			mPerspective = b;

			_updateProjectionMatrix();
		}
	}

	void Camera::setCullingMask(u32 cullingMask)
	{
		mCullingMask = cullingMask;
	}
}
